/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Initial Developer of the Original Code is
*   Nick Kreeger
* Portions created by the Initial Developer are Copyright (C) 2006-2007
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*   Nick Kreeger <nick.kreeger@park.edu>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */

#import "ContactAutoCompleteTextField.h"
#import "ContactsAutoCompleteDataSource.h"
#import "PopUpImageView.h"
#import "CHBrowserService.h"
#import "NSString+Utils.h"

#include "nsAbBaseCID.h"
#include "nsString.h"
#include "nsIAbAutoCompleteSession.h"
#include "nsIAutoCompleteResults.h"
#include "nsIAutoCompleteListener.h"

static const int kMaxRows = 10;
static const int kFrameMargin = 1;

@interface AutoCompleteWindow : NSWindow

-(BOOL)isKeyWindow;

@end

@implementation AutoCompleteWindow

-(BOOL)isKeyWindow
{
  return YES;
}

@end

#pragma mark -

/**
* Text-cell subclass that calculates the current editable region of textfield
 * with contacts.
 */
@interface ContactAutoCompleteTextFieldCell : NSTextFieldCell
{
  //XXX write me
}

@end

@implementation ContactAutoCompleteTextFieldCell

@end


@interface ContactAutoCompleteTextField(Private)

-(NSTableView*)tableView;
-(int) visibleRows;

-(void)dataReady:(nsIAutoCompleteResults*)aResults status:(AutoCompleteStatus)aStatus;
-(void)initAutoCompleteSession;
-(void)cleanup;

-(void)startSearch:(NSString*)aString complete:(BOOL)aComplete;
-(void)performSearch;
-(BOOL)caretIsAtEndOfLine;
-(void)dataReady:(nsIAutoCompleteResults*)aResults status:(AutoCompleteStatus)aStatus;
-(void)searchTimer:(NSTimer*)aTimer;

-(void)openPopup;
-(void)resizePopup:(BOOL)forceResize;
-(void)closePopup;
-(BOOL)isPopupWindowVisible;

-(void)onRowClicked:(NSNotification *)aNote;
-(void)onBlur:(NSNotification *)aNote;
-(void)onResize:(NSNotification *)aNote;
-(void)onUndoOrRedo:(NSNotification *)aNote;
-(void)shutdown:(NSNotification*)aNotification;

-(void)selectRowBy:(int)aRows;
-(void)selectRowAt:(int)aRow;
-(void)completeSelectedResult;
-(void)completeResult:(int)aRow;

@end

@implementation ContactAutoCompleteTextField

+(Class)cellClass
{
  return [ContactAutoCompleteTextFieldCell class];
}

// This method shouldn't be necessary according to the docs. The superclass's
// constructors should read in the cellClass and build a properly configured
// instance on their own. Instead they ignore cellClass and build a NSTextFieldCell.
-(id)initWithCoder:(NSCoder*)coder
{
  [super initWithCoder:coder];
  ContactAutoCompleteTextFieldCell* cell = [[[ContactAutoCompleteTextFieldCell alloc] initTextCell:@""] autorelease];
  [cell setEditable:[self isEditable]];
  [cell setDrawsBackground:[self drawsBackground]];
  [cell setBordered:[self isBordered]];
  [cell setBezeled:[self isBezeled]];
  [cell setScrollable:YES];
  [self setCell:cell];
  
  return self;
}

-(void)awakeFromNib
{
  NSTableColumn* column;
  NSScrollView* scrollView;
  
  [self setFont:[NSFont controlContentFontOfSize:0]];
  [self setDelegate:self];
    
  // construct and configure the popup window
  mPopupWindow = [[AutoCompleteWindow alloc] initWithContentRect:NSMakeRect(0,0,0,0)
                                                       styleMask:NSBorderlessWindowMask
                                                         backing:NSBackingStoreBuffered 
                                                           defer:NO];
  [mPopupWindow setReleasedWhenClosed:NO];
  [mPopupWindow setHasShadow:YES];
  [mPopupWindow setAlphaValue:0.9];
  
  // construct and configure the view
  mTableView = [[[NSTableView alloc] initWithFrame:NSMakeRect(0,0,0,0)] autorelease];
  [mTableView setIntercellSpacing:NSMakeSize(1, 2)];
  [mTableView setTarget:self];
  [mTableView setAction:@selector(onRowClicked:)];
  [mTableView setHeaderView:nil];
  
  [self setAutoresizesSubviews:YES];
  
  // create the text columns
  column = [[[NSTableColumn alloc] initWithIdentifier:@"col1"] autorelease];
  [mTableView addTableColumn:column];
  //column = [[[NSTableColumn alloc] initWithIdentifier:@"col2"] autorelease];
  //[[column dataCell] setTextColor:[NSColor darkGrayColor]];
  //[mTableView addTableColumn:column];
  
  // construct the scroll view that contains the table view
  scrollView = [[[NSScrollView alloc] initWithFrame:NSMakeRect(0,0,0,0)] autorelease];
  [scrollView setHasVerticalScroller:YES];
  [[scrollView verticalScroller] setControlSize:NSSmallControlSize];
  [scrollView setDocumentView:mTableView];
  
  // init the datasource
  mDataSource = [[ContactsAutoCompleteDataSource alloc] initWithContactAutoCompleteTextField:self];
  [mTableView setDataSource:mDataSource];
  
  [mPopupWindow setContentView:scrollView];
  
  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(onResize:)
                                               name:NSWindowDidResizeNotification
                                             object:nil];
  
  // listen for Undo/Redo to make sure autocomplete doesn't get horked.
  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(onUndoOrRedo:)
                                               name:NSUndoManagerDidRedoChangeNotification
                                             object:[[self fieldEditor] undoManager]];
  
  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(onUndoOrRedo:)
                                               name:NSUndoManagerDidUndoChangeNotification
                                             object:[[self fieldEditor] undoManager]];
  
  // register for embedding shutting down, to clean up history stuff
  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(shutdown:)
                                               name:TermEmbeddingNotificationName
                                             object:nil];
  
  //[self registerForDraggedTypes:[NSArray arrayWithObjects:kCorePasteboardFlavorType_url, NSURLPboardType, NSStringPboardType, nil]];
}

-(void)dealloc
{
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  [self cleanup];
  [super dealloc];
}

-(void)cleanup
{
  [mSearchString release];
  mSearchString = nil;
  
  [mPopupWindow release];
  mPopupWindow = nil;
  
  [mDataSource release];
  mDataSource = nil;
}

-(void)shutdown:(NSNotification*)aNotification
{
  [self cleanup];
}

-(id)fieldEditor
{
  return [[self window] fieldEditor:NO forObject:self];
}

-(void)setStringUndoably:(NSString*)aString fromLocation:(unsigned int)aLocation
{
  NSTextView* fieldEditor = [self fieldEditor];
  NSString* curValue = [fieldEditor string];
  
  unsigned curLength = [curValue length];
  if (aLocation > curLength)    // sanity check or AppKit crashes
    return;
  
  if ((aLocation + [aString length] == curLength) && [curValue compare:aString options:0 range:NSMakeRange(aLocation, [aString length])] == NSOrderedSame)
    return;  // nothing to do
  
  NSRange range = NSMakeRange(aLocation, [curValue length] - aLocation);
  if ([fieldEditor shouldChangeTextInRange:range replacementString:aString])
  {
    [[fieldEditor textStorage] replaceCharactersInRange:range withString:aString];
    if (NSMaxRange(range) == 0) // will only be true if the field is empty
      [fieldEditor setFont:[self font]]; // wrong font will be used otherwise
    
    // Whenever we send [self didChangeText], we trigger the
    // textDidChange method, which will begin a new search with
    // a new search string (which we just inserted) if the selection
    // is at the end of the string.  So, we "select" the first character
    // to prevent that badness from happening.
    [fieldEditor setSelectedRange:NSMakeRange(0,0)];    
    [fieldEditor didChangeText];
  }
  
  // sanity check and don't update the highlight if we're starting from the
  // beginning of the string. There's no need for that since no autocomplete
  // result would ever replace the string from location 0.
  if (aLocation > [[fieldEditor string] length] || !aLocation)
    return;
  range = NSMakeRange(aLocation, [[fieldEditor string] length] - aLocation);
  [fieldEditor setSelectedRange:range];
}


-(NSTableView*)tableView
{
  return mTableView;
}

-(int)visibleRows
{
  int minRows = [mDataSource rowCount];
  return minRows < kMaxRows ? minRows : kMaxRows;
}

#pragma mark -
#pragma mark Searching

-(void)startSearch:(NSString*)aString complete:(BOOL)aComplete
{
  [mDataSource setSearchString:aString];
  mCompleteResult = aComplete;
  
  if ([self isPopupWindowVisible])
    [mDataSource startSearch];
  
  else
  {
    if (mOpenTimer)
    {
      [mOpenTimer invalidate];
      [mOpenTimer release];
    }
    
    mOpenTimer = [[NSTimer scheduledTimerWithTimeInterval:0.2 
                                                   target:self 
                                                 selector:@selector(searchTimer:)
                                                 userInfo:nil 
                                                  repeats:NO] retain];
  }
}

-(BOOL)caretIsAtEndOfLine
{
  NSRange selectedLocation = [[self fieldEditor] selectedRange];
  return (selectedLocation.length == 0) && (selectedLocation.location == [[self stringValue] length]);
}

-(void)onDataAvailable
{
  if ([mDataSource rowCount] > 0)
  {
    [mTableView noteNumberOfRowsChanged];
    [self openPopup];
  }
  else
    [self closePopup];
}

-(void)searchTimer:(NSTimer*)aTimer
{
  [mOpenTimer release];
  mOpenTimer = nil;
  
  [mDataSource startSearch];
}

-(void)clearResults
{
  [mDataSource clearResults];
  
  [self closePopup];
}

#pragma mark -
#pragma mark Popup Handling

-(void)openPopup
{
  [self resizePopup:YES];
  
  // show the popup
  if ([mPopupWindow isVisible] == NO)
  {
    [[self window] addChildWindow:mPopupWindow ordered:NSWindowAbove];
    [mPopupWindow orderFront:nil];
    [mTableView setDataSource:mDataSource];
  }
}

-(void)closePopup
{
  [mTableView setDataSource:nil];
  [[mPopupWindow parentWindow] removeChildWindow:mPopupWindow];
  [mPopupWindow orderOut:nil];
}

-(void)resizePopup:(BOOL)forceResize
{
  if ([self visibleRows] == 0)
  {
    [self closePopup];
    return;
  }
  
  // don't waste time resizing stuff that is not visible (unless we're about
  // to show the popup)
  if (![self isPopupWindowVisible] && !forceResize)
    return;
  
  // get the origin of the location bar in coordinates of the root view
  NSRect locationFrame = [self bounds];
  NSPoint locationOrigin = locationFrame.origin;
  locationOrigin.y += NSHeight(locationFrame);    // we want bottom left
  locationOrigin = [self convertPoint:locationOrigin toView:nil];
  
  // get the height of the table view
  NSRect winFrame = [[self window] frame];
  int tableHeight = (int)([mTableView rowHeight] + [mTableView intercellSpacing].height) * [self visibleRows];
  
  // make the columns split the width of the popup
  [[mTableView tableColumnWithIdentifier:@"col1"] setWidth:locationFrame.size.width];
  
  // position the popup anchored to bottom/left of location bar
  NSRect popupFrame = NSMakeRect(winFrame.origin.x + locationOrigin.x + kFrameMargin,
                                 ((winFrame.origin.y + locationOrigin.y) - tableHeight) - kFrameMargin,
                                 locationFrame.size.width - (2 * kFrameMargin),
                                 tableHeight);
  
  [mPopupWindow setFrame:popupFrame display:NO];
}

-(BOOL)isPopupWindowVisible
{
  return [mPopupWindow isVisible];
}

#pragma mark -
#pragma mark Contact Completion

/**
 * Returns whether the user has typed anything into the url bar since the last 
 * time the url was set (by loading a page). We know this is the case by looking
 * at if there is a search string.
 */
-(BOOL)userHasTyped
{
  return (mSearchString != nil);
}

-(void)enterResult:(int)aRow
{
  if (aRow >= 0 && [mDataSource rowCount] > 0) 
  {
    [self setStringUndoably:[mDataSource resultForRow:[mTableView selectedRow] columnIdentifier:@"col1"] fromLocation:0];
    [self closePopup];
  } 
  else if (mOpenTimer) 
  {
    // if there was a search timer going when we hit enter, cancel it
    [mOpenTimer invalidate];
    [mOpenTimer release];
    mOpenTimer = nil;
  }
}

-(void)revertText
{
  [self clearResults];
  NSTextView *fieldEditor = [self fieldEditor];
  [[fieldEditor undoManager] removeAllActionsWithTarget:fieldEditor];
  [fieldEditor setString:@""];
  [fieldEditor selectAll:self];
}

#pragma mark -
#pragma mark Event Handlers

-(void)onRowClicked:(NSNotification*)aNote
{
  [self enterResult:[mTableView clickedRow]];
  //[[[self window] windowController] goToLocationFromToolbarURLField:self];
}

-(void)onBlur:(NSNotification*)aNote
{
  // close up the popup and make sure we clear any past selection. We cannot
  // use |selectAt:-1| because that would show the popup, even for a brief instant
  // and cause flashing.
  [mTableView deselectAll:self];
  [self closePopup];
}

-(void)onResize:(NSNotification*)aNote
{
  if ([aNote object] == [self window])
    [self resizePopup:NO];
}

-(void)onUndoOrRedo:(NSNotification*)aNote
{
  [mTableView deselectAll:self];
  [self clearResults];
}

#pragma mark -
#pragma mark NSTextField Delegate

- (void)controlTextDidChange:(NSNotification *)aNote
{
  NSTextView *fieldEditor = [[aNote userInfo] objectForKey:@"NSFieldEditor"];
  // we are here either because the user is typing or they are selecting
  // an item in the autocomplete popup. When they are typing, the location of
  // the selection will be non-zero (wherever the insertion point is). When
  // they autocomplete, the length and the location will both be zero.
  // We use this info in the following way: if they are typing or backspacing,
  // restart the search and no longer use the selection in the popup.
  NSRange range = [fieldEditor selectedRange];
  NSString* currentText = [fieldEditor string];
  if ([currentText length] && range.location) {
    // when we ask for a NSTextView string, Cocoa returns
    // a pointer to the view's backing store.  So, the value
    // of the string continually changes as we edit the text view.
    // Since we'll edit the text view as we add in autocomplete results,
    // we've got to make a copy of the string as it currently stands
    // to know what we were searching for in the first place.
    NSString *searchString = [NSString stringWithString:currentText];
    [self startSearch:searchString complete:!mBackspaced];
  }
  else if (([mTableView selectedRow] == -1) || mBackspaced)
    [self clearResults];
  
  mBackspaced = NO;
}

- (void)controlTextDidEndEditing:(NSNotification *)aNote
{
  [self clearResults];
  id fieldEditor = [[aNote userInfo] objectForKey:@"NSFieldEditor"];
  [[fieldEditor undoManager] removeAllActionsWithTarget:fieldEditor];
}

-(BOOL)control:(NSControl*)control textView:(NSTextView*)textView doCommandBySelector:(SEL)command
{
  if (command == @selector(insertNewline:)) 
  {
    [self enterResult:[mTableView selectedRow]];
    [mTableView deselectAll:self];
  }
  else if (command == @selector(moveUp:)) 
  {
    if ([self isPopupWindowVisible]) 
    {
      [self selectRowBy:-1];
      [self completeSelectedResult];
      return YES;
    }
  } 
  else if (command == @selector(moveDown:)) 
  {
    if ([self isPopupWindowVisible]) 
    {
      [self selectRowBy:1];
      [self completeSelectedResult];
      return YES;
    }
    else if ([self caretIsAtEndOfLine])
    {
      [self startSearch:[self stringValue] complete:YES];
      return YES;
    }
  }
  else if (command == @selector(scrollPageUp:))
  {
    [self selectRowBy:-kMaxRows];
    [self completeSelectedResult];
  } 
  else if (command == @selector(scrollPageDown:)) 
  {
    [self selectRowBy:kMaxRows];
    [self completeSelectedResult];
  } 
  else if (command == @selector(moveToBeginningOfDocument:)) 
  {
    [self selectRowAt:0];
    [self completeSelectedResult];
  } 
  else if (command == @selector(moveToEndOfDocument:)) 
  {
    [self selectRowAt:[mTableView numberOfRows] - 1];
    [self completeSelectedResult];
  } 
  else if (command == @selector(complete:)) 
  {
    [self selectRowBy:1];
    [self completeSelectedResult];
    return YES;
  } 
  else if (command == @selector(insertTab:)) 
  {
    if ([mPopupWindow isVisible]) 
    {
      [self selectRowBy:1];
      [self completeSelectedResult];
      return YES;
    } 
    else 
    {
      // use the normal key view unless we know more about our siblings and have
      // explicitly nil'd out the next key view. In that case, select the window
      // to break out of the toolbar and tab through the rest of the window
      if ([self nextKeyView])
        [[self window] selectKeyViewFollowingView:self];
      else 
      {
        NSWindow* wind = [self window];
        [wind makeFirstResponder:wind];
      }
    }
  }
  else if (command == @selector(deleteBackward:) || command == @selector(deleteForward:)) 
  {
    // if the user deletes characters, we need to know so that
    // we can prevent autocompletion later when search results come in
    if ([[textView string] length] > 1) 
    {
      [self selectRowAt:-1];
      mBackspaced = YES;
    }
  }
  
  return NO;
}

-(void)selectRowBy:(int)aRows
{
  int row = [mTableView selectedRow];
  
  if (row == -1 && aRows < 0) 
  {
    // if nothing is selected and you scroll up, go to last row
    row = [mTableView numberOfRows] - 1;
  } 
  else if (row == [mTableView numberOfRows] - 1 && aRows == 1) 
  {
    // if the last row is selected and you scroll down, do nothing. pins
    // the selection at the bottom.
  } 
  else if (aRows + row < 0) 
  {
    // if you scroll up beyond first row...
    if (row == 0)
      row = -1; // ...and first row is selected, select nothing
    else
      row = 0; // ...else, go to first row
  } 
  else if (aRows + row >= [mTableView numberOfRows])
  {
    // if you scroll down beyond the last row...
    if (row == [mTableView numberOfRows] - 1)
      row = 0; // and last row is selected, select first row
    else
      row = [mTableView numberOfRows] - 1; // else, go to last row
  } 
  else 
  {
    // no special case, just increment current row
    row += aRows;
  }
  
  [self selectRowAt:row];
}

-(void)selectRowAt:(int)aRow
{
  if (aRow >= -1 && [mDataSource rowCount] > 0) 
  {
    // show the popup
    [self openPopup];
    
    if ( aRow == -1 )
      [mTableView deselectAll:self];
    else 
    {
      [mTableView selectRow:aRow byExtendingSelection:NO];
      [mTableView scrollRowToVisible: aRow];
    }
  }
}

#pragma mark -
#pragma mark Object Selection

-(void)completeSelectedResult
{
  [self completeResult:[mTableView selectedRow]];
}

-(void)completeResult:(int)aRow
{
  if (aRow < 0 && mSearchString) 
  {
    // reset to the original search string with the insertion point at the end. Note
    // we have to make our range before we call setStringUndoably: because it calls
    // clearResults() which destroys |mSearchString|.
    NSRange selectAtEnd = NSMakeRange([mSearchString length], 0);
    [self setStringUndoably:mSearchString fromLocation:0];
    [[self fieldEditor] setSelectedRange:selectAtEnd];
  }
  else 
  {
    if ([mDataSource rowCount] <= 0)
      return;
    
    /*
     This needs to be changed to work with email addrs.
    
    // Fill in the suggestion from the list, but change only the text 
    // after what is typed and select just that part. This allows the
    // user to see what they have typed and what change the autocomplete
    // makes while allowing them to continue typing w/out having to
    // reset the insertion point. 
    NSString* result = [mDataSource resultForRow:aRow columnIdentifier:@"col1"];
    
    // figure out where to start the match, depending on whether the user typed the protocol part
    int protocolLength = 0;
    NSURL* resultURL = [NSURL URLWithString:result];
    NSURL* searchURL = [NSURL URLWithString:mSearchString];
    if (resultURL)
    {
      if (([[searchURL scheme] length] == 0) || ![[searchURL scheme] isEqualToString:[resultURL scheme]])
        protocolLength = [[resultURL scheme] length];
    }
    
    NSRange matchRange = [result rangeOfString:mSearchString 
                                       options:NSCaseInsensitiveSearch
                                         range:NSMakeRange(protocolLength, [result length] - protocolLength)];
    if (matchRange.length > 0 && matchRange.location != NSNotFound) 
    {
      unsigned int location = matchRange.location + matchRange.length;
      result = [result substringWithRange:NSMakeRange(location, [result length]-location)];
      [self setStringUndoably:result fromLocation:[mSearchString length]];
    }
     */
  }
}

@end
